package pl.edu.agh.neurony.logic;

import java.util.ArrayList;
import java.util.List;

import pl.edu.agh.neurony.exceptions.BadNeuronInitiationException;
import pl.edu.agh.neurony.exceptions.LayerConstructionException;
import pl.edu.agh.neurony.exceptions.ProhibitedOperationException;

public abstract class Layer {
	
	protected Double basicLearningFactor;
	protected Double learningFactor;
	public void setLearningFactor(Double learningFactor) {this.learningFactor=learningFactor; this.basicLearningFactor = learningFactor;}
	public Double getLearningFactor() {return learningFactor;}

	protected List<Neuron> neurons = new ArrayList<Neuron>();
	public List<Neuron> getNeurons() {return neurons;}
	public void setNeurons(List<Neuron> neurons) {this.neurons = neurons;}

	public final int layerNr;
	public final static int INPUT_LAYER = 0;
	public final static int OUTPUT_LAYER = -1;
	
	//constructors
	public Layer(int layerNumber, Neuron neuron) throws LayerConstructionException {
		if (layerNumber < 1 && layerNumber!=OUTPUT_LAYER)
			throw new LayerConstructionException("Layer number should be an integer greater than or equal to 1");
		if (neuron == null)
			throw new LayerConstructionException(new NullPointerException("Passed neuron cannot be null"));
		
		layerNr = layerNumber;
		neurons.add(neuron);
	}

	//public methods
	void createOutputConnectionsToNeuron(Neuron neuron) {
		for (Neuron layerNeuron : neurons)
			layerNeuron.connectOutputNeuron(neuron);
	}

	void createInputConnectionsToNeuron(Neuron neuron) {
		for (Neuron layerNeuron : neurons)
			layerNeuron.connectInputNeuron(neuron);
	}

	List<Neuron> disconnectFromUpperLayer() {
		for (Neuron neuron : neurons)
			for (Connection connection : neuron.getOutputConnections())
				connection.disconnect();
		
		return neurons;
	}
	
	List<Neuron> disconnectFromLowerLayer() {
		for (Neuron neuron : neurons)
			for (Connection connection : neuron.getInputConnections())
				connection.disconnect();
		
		return neurons;
	}
	
	void connectWithUpperLayer(Layer layer) {
		for (Neuron outputNeuron : layer.neurons)
				createOutputConnectionsToNeuron(outputNeuron);
	}
	
	void connectWithLowerLayer(Layer layer) {
		for (Neuron inputNeuron : layer.neurons)
				createInputConnectionsToNeuron(inputNeuron);
	}
	
	Neuron removeLastNeuron() throws ProhibitedOperationException {
		if (neurons.size() < 2)
			throw new ProhibitedOperationException("Each layer has to have at least one neuron");
		
		Neuron neuron = neurons.remove(neurons.size()-1);
		for (Connection connection : neuron.getOutputConnections())
			connection.disconnect();
		for (Connection connection : neuron.getInputConnections())
			connection.disconnect();
		
		return neuron;
	}
	
	void addNeuron(Neuron neuron) throws LayerConstructionException {
		if (neuron.getInputConnections().size() < 1)
			// || neuron.getOutputConnections().size() < 1
			throw new LayerConstructionException("Adding unconnected neuron to layer is prohibited");
			
		neurons.add(neuron);
	}
	
	public int size() {
		return neurons.size();
	}
	
	public boolean isValid() {
		for (Neuron neuron : neurons)
			if (!neuron.isValid())
				return false;
		
		return true;
	}
	
	public void fillLayerWithValues(List<Double> values) {
		for(int i=0;i<values.size();i++) {
			neurons.get(i).bias = values.get(i);
		}
	}
	
	public void resetValues(boolean resetOutputs, boolean resetErrors) {
		for (Neuron n: neurons) {
			if (resetOutputs)
				n.resetOutputValue();
			if (resetErrors)
				n.error = null;
		}
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Layer:" + layerNr +"\n");
		for (Neuron n : neurons) {
			sb.append(n.toString() + "\n");
		}
		return sb.toString();
	}

	public void assignRandomWeights(double from, double to) {
		for (Neuron n : neurons) {
			n.assignRandomWeights(from, to);
		}
	}
	
	public void updateLearningFactor(int step, int learningSteps) {
		learningFactor = basicLearningFactor - ((double) step * basicLearningFactor / (double) learningSteps);
	}
	
	public String displayInfo() {
		String info = "\tLayer " + layerNr + "\n";
		for (Neuron neuron : neurons) {
			info += "\t\t" + neuron.displayInfo();
		}
		info += "\n";
		return info;
	}
	

	public abstract void learn(int step, int steps) throws BadNeuronInitiationException;
	public abstract void setLearningParams(Number... arguments);
}
